package partie4.router;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import partie4.threadManager.ThreadManager;
import jpcap.JpcapCaptor;
import jpcap.JpcapSender;
import jpcap.NetworkInterface;
import jpcap.packet.ARPPacket;
import jpcap.packet.EthernetPacket;
import jpcap.packet.IPPacket;

/**
 * Classe principale contenant toutes les sous-classes et servant a simuler le comportement d'un routeur.
 */
public class Router {
	private static Router instance; //instance unique de la classe
	private NetworkInterface[] devices; //interfaces reseau
	private boolean quitter; //vrai s'il faut quitter l'application
	private JpcapSender sender; //permet d'envoyer des paquets
	private RouterTable table; //table de routage
	private ThreadManager manager; //gestionnaire des threads
	
	/**
	 * Obtenir l'instance unique de la classe (Singleton).
	 * @return instance unique
	 */
	public static Router getInstance(){
		if (instance == null){
			instance = new Router();
		}
		
		return instance;
	}
	
	/**
	 * Constructeur prive.
	 */
	private Router(){
		this.quitter = false;		
	}
	
	/**
	 * Obtenir la liste des interfaces.
	 */
	public void getDeviceList(){
		this.devices = JpcapCaptor.getDeviceList();
	}
	
	/**
	 * Creer un capteur de paquet pour une interface donnee.
	 * @param device L'interface
	 * @return Le capteur
	 */
	public JpcapCaptor openDevice(NetworkInterface device){
		JpcapCaptor captor = null;		
		
		try {
			captor = JpcapCaptor.openDevice(device, 65535, false, 20);
		} catch (IOException e) {
			System.out.println("Erreur lors de l'ouverture de l'interface reseau.");
			e.printStackTrace();
		}		
		
		return captor;
	}
	
	/**
	 * Determine si une IP est l'une des IP d'une interface.
	 * @param device L'interface
	 * @param ip L'IP
	 * @return Vrai s'il s'agit d'une IP de l'interface, faux sinon.
	 */
	public boolean isMyIP(NetworkInterface device, String ip){
		boolean myIp = false;
		
		for (int i = 0; i < device.addresses.length; i++){
			if (device.addresses[i].address.getHostAddress().equals(ip)){
				myIp = true;
			}
		}
		
		return myIp;
	}
	
	/**
	 * Afficher les interfaces (pour tests).
	 */
	public void showDevices(){			
		for(int i=0;i<this.devices.length;i++){
			System.out.println(i+":"+devices[i].name+"("+devices[i].description+")");
			for (int j = 0; j < this.devices[i].addresses.length;j++){
				System.out.println(this.devices[i].addresses[j].address.getHostAddress());
			}
			System.out.print("IP: ");
			System.out.println("MAC: " + this.macToString(devices[i].mac_address));			
			
		}		
	}
	
	/**
	 * Demarre la capture de paquets pour un capteur.
	 * @param captor Le capteur
	 */
	public void capturePackets(JpcapCaptor captor, NetworkInterface device){		
		while (!this.quitter){
			captor.processPacket(-1, new Receiver(device));
		}
		captor.close();
	}
	
	/**
	 * Fonction d'execution principale.
	 */
	public void run(){
		//lire table de routage
		try {
			this.table = new RouterTable();
		} catch (FileNotFoundException e) {
			System.out.println("Le fichier rout_table.txt est introuvable.");
			e.printStackTrace();
		} catch (IOException e) {
			System.out.println("Erreur lors de la lecture de la table de routage.");
			e.printStackTrace();
		}		
		
		//obtenir les interfaces reseau
		this.getDeviceList();	
		
		//demarrer un thread pour chaque interface
		this.startThreads();		
	}
	
	/**
	 * Demarre les threads (un thread par interface).
	 */
	private void startThreads(){
		this.manager = new ThreadManager();
		
		for (NetworkInterface device : this.devices){
			this.manager.addInterfaceThread(device);
		}
        
        this.manager.startAllInterface();
	}
	
	/**
	 * Envoie un paquet IP.
	 * @param packet Paquet a envoyer.
	 * @param inte Interface sur lequel envoyer le paquet.
	 */
	public void sendPacket(IPPacket packet, NetworkInterface inte){
		
		//modifier l'entete ethernet
		packet = this.changeHeader(packet, inte);	
		
			//envoyer le paquet
			if (packet != null){
			try {
				this.sender = JpcapSender.openDevice(inte);
				this.sender.sendPacket(packet);
				this.sender.close();
			} catch (IOException e) {
				System.out.println("Erreur lors de l'ouverture de l'interface reseau.");
				e.printStackTrace();
			}			
		}
	}	
	
	/**
	 * Envoie un paquet ARP.
	 * @param packet Paquet a envoyer.
	 * @param inte Interface sur lequel envoyer le paquet.
	 */
	public void sendPacket(ARPPacket packet, NetworkInterface inte){
		//modifier le paquet ARP	
		packet = this.editPacket(packet, inte);		
				
		//envoyer le paquet 
		if (packet != null){
			try {
				this.sender = JpcapSender.openDevice(inte);
				this.sender.sendPacket(packet);
				this.sender.close();
			} catch (IOException e) {
				System.out.println("Erreur lors de l'ouverture de l'interface reseau.");
				e.printStackTrace();
			}
		}
	}	
	
	/**
	 * Modifie un paquet ARP.
	 * @param packet Le paquet a modifier.
	 * @param inte Interface ayant recu le paquet.
	 * @return Le paquet modifie, null s'il ne faut pas envoyer le paquet
	 */
	
	private ARPPacket editPacket(ARPPacket packet, NetworkInterface inte){			
		byte[] protSrc = packet.sender_protoaddr; //IP source
		byte[] protDest = packet.target_protoaddr; //IP destination
		byte[] macSrc = packet.sender_hardaddr;	//MAC source
		String inteMac = this.macToString(inte.mac_address).toUpperCase(); //adresse MAC de l'interface	
		String destIP = this.removeSlash(packet.getTargetProtocolAddress().toString());
		String destMac = this.table.getTable().get(destIP).toUpperCase(); //addresse MAC dest du paquet selon table de routage
		
		//IP sur meme interface que requete
		if (inteMac.equals(destMac)){
			packet = null; //ne pas envoyer le paquet
		}			
		else{ //la requete est pour une IP qui n'est pas sur l'interface		
			packet.operation = ARPPacket.ARP_REPLY;
			packet.sender_hardaddr = inte.mac_address; //MAC de l'interface ayant recu la requete
			packet.sender_protoaddr = protDest;
			packet.target_hardaddr = macSrc; //renvoyer a la source
			packet.target_protoaddr = protSrc;
		}	
		
		return packet;
	}
	
	/**
	 * Convertir une adresse mac en string.
	 * @param mac L'adresse mac en byte.
	 * @return L'adresse mac en string.
	 */
	private String macToString(byte[] mac){
	    StringBuilder sb = new StringBuilder(18);
	    for (byte b : mac) {
	        if (sb.length() > 0)
	            sb.append(':');
	        sb.append(String.format("%02x", b));
	    }
	    return sb.toString();
	}
	
	/**
	 * Modifie l'entete Ethernet d'un paquet.
	 * @param packet Le paquet a modifier.
	 * @param inte Interface ayant recu le paquet.
	 * @return Le paquet modifie.
	 */
	private IPPacket changeHeader(IPPacket packet, NetworkInterface inte){	
		byte[] dstMac = ((EthernetPacket)packet.datalink).dst_mac; //mac de destination du paquet
		byte[] srcMac = ((EthernetPacket)packet.datalink).src_mac; //mac source du paquet
		String inteMac = this.macToString(inte.mac_address).toUpperCase(); //adresse MAC de l'interface	
		String dstMacStr = this.macToString(dstMac).toUpperCase(); //mac de destination en string
		String dstIpStr = this.removeSlash(packet.dst_ip.getHostAddress()); //IP de destination en string
		String srcIpStr = this.removeSlash(packet.src_ip.getHostAddress()); //IP source en string
		
		//le paquet lui est partiellement destine (MAC mais non IP)
		if (inteMac == dstMacStr && !this.isMyIP(inte, dstIpStr)){
			//le paquet provient de l'interface ou il doit etre achemine - rien a faire
			if (this.table.getTable().get(dstIpStr).equals(this.table.getTable().get(srcIpStr))){
				packet = null;
			}
			else{ 		
				//decrementer le TTL
				packet.hop_limit--;
				
				//si le TTL est 0 ne pas transmettre
				if (packet.hop_limit < 1){
					packet = null;
				}
				else{ 
					//creer une nouvelle entete ethernet
					EthernetPacket ether = new EthernetPacket();
					ether.frametype = EthernetPacket.ETHERTYPE_IP;
				
					//adresse mac source
					ether.src_mac = inte.mac_address;
					
					//adresse mac destination
					ether.dst_mac = this.macToByte(this.table.getTable().get(dstIpStr));
					
					//attacher l'entete ethernet au paquet
					packet.datalink = ether;
				}
			}
		}
		else{ //le paquet lui est completement destine (MAC et IP) ou ne lui est pas destine
			packet = null; //ne pas envoyer le paquet
		}
		return packet;
	}
	
	/**
	 * Convertie une adresse mac en byte.
	 * @param macAdr Adresse mac en String
	 * @return Adresse mac en byte
	 */
	public byte[] macToByte(String macAddress){
        String[] bytes = macAddress.split(":");
        byte[] parsed = new byte[bytes.length];

        for (int x = 0; x < bytes.length; x++)
        {
            BigInteger temp = new BigInteger(bytes[x], 16);
            byte[] raw = temp.toByteArray();
            parsed[x] = raw[raw.length - 1];
        }
        
        return parsed;
	}
	
	/**
	 * Obtient l'adresse Mac du routeur.
	 * @return Adresse Mac du routeur.
	 */
	private byte[] getMacSource(){
		InetAddress ip;
		byte[] mac = null;
		
		try {	 
			ip = InetAddress.getLocalHost();	 
			java.net.NetworkInterface network = java.net.NetworkInterface.getByInetAddress(ip);			
			mac = network.getHardwareAddress();	 
			
		} catch (java.net.UnknownHostException e) {	 
			System.out.println("Erreur lors de l'obtention de l'adresse Mac du routeur.");
			e.printStackTrace();	 
		} catch (java.net.SocketException e){	
			System.out.println("Erreur lors de l'obtention de l'adresse Mac du routeur.");
			e.printStackTrace();	 
		}
		
		return mac;
	}
	
	/**
	 * Obtient l'adresse IP du routeur.
	 * @return Adresse IP du routeur.
	 */
	private byte[] getIPSource(){
		InetAddress ip = null;
		
		try {	 
			ip = InetAddress.getLocalHost();
		} catch (java.net.UnknownHostException e) {	 
			System.out.println("Erreur lors de l'obtention de l'adresse Mac du routeur.");
			e.printStackTrace();	 
		} 

		return ip.getAddress();
	}
	
	/**
	 * Enleve le premier backslash d'une chaine de caracteres, si present.
	 * Souvent Jpcap renvoie des adresses debutant par un backslash.
	 */
	private String removeSlash(String str){
		if (str.contains("/")){
			str = str.substring(1,str.length());
		}
		
		return str;
	}
	
}
